import wx
from backups import gui, db, util, mail
import os
import datetime
import traceback
import platform
import ctypes


def pretty_number(number):
    index = 0
    while number > 1024:
        number = number / 1024
        index += 1
        
    table = {
        0:'',
        1:'k',
        2:'M',
        3:'G',
        4:'T'
    }
    
    return '%i%s'%(number,table.get(index,'-'))

def catch_all_and_log(show_error=False):
    def wrap(func):
        def wrapper(*a, **kw):
            try:
                return func(*a, **kw)
            except Exception, e:
                trace = traceback.format_exc()
                if show_error:
                    try:
                        msg = str(e)
                        dlg = wx.MessageDialog(None, msg, 'Uncaught error, operation not completed', wx.OK|wx.ICON_WARNING)
                        dlg.ShowModal()
                    except Exception, inner:
                        logger.msg(inner)
                logger.msg('Uncaught exception: ',e)
                logger.msg('Traceback follows')
                logger.msg(trace)
        return wrapper
    return wrap

class Controller(object):
    mainframe = None
    def __init__(self, using_gui=True):
        code_dir = __file__.rsplit(os.path.sep, 1)[0]
        while not os.path.isdir(code_dir): # find the first parent directory to save the database in.
            code_dir = code_dir.rsplit(os.path.sep, 1)[0]
        self.dc = db.DataController(os.path.join(code_dir,'backups.db'))
        self.bc = BackupController(self.dc, using_gui)
        if using_gui:
            self.app = wx.PySimpleApp(0)
            self.mainframe = gui.MainFrame(None, -1, "")
            self.mainframe.Show()
            self.app.SetTopWindow(self.mainframe)
            wx.InitAllImageHandlers()
            self.install_handlers()
            self.update_gui()

    def install_handlers(self):
        self.mainframe.Bind(wx.EVT_BUTTON, self.choose_destination, self.mainframe.choose_destination)
        self.mainframe.Bind(wx.EVT_BUTTON, self.add_source, self.mainframe.add_source)
        self.mainframe.Bind(wx.EVT_BUTTON, self.remove_source, self.mainframe.remove_source)
        self.mainframe.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGING, self.generate_statistics, self.mainframe.notebook)
        self.mainframe.Bind(wx.EVT_MENU, self.run, self.mainframe.run)
        #self.mainframe.Bind(wx.EVT_MENU, self.clear_log, self.mainframe.clear_log)
        self.mainframe.Bind(wx.EVT_MENU, lambda evt: wx.Exit(), self.mainframe.quit)
        self.mainframe.Bind(wx.EVT_MENU, self.email_settings, self.mainframe.email_settings)
        logger.observers.append(self.logmsg)
        #import sys
        #logger.observers.append(sys.stdout.write)
        def update_statusbar(text):
            if isinstance(text, unicode):
                self.mainframe.mainframe_statusbar.SetStatusText(text, 0)
            else:
                self.mainframe.mainframe_statusbar.SetStatusText(unicode(text, errors='ignore'), 0)
        logger.observers.append(update_statusbar)

    def logmsg(self, msg):
        #self.mainframe.log.AppendText(msg+'\n')
        if isinstance(msg, unicode):
            self.mainframe.log.AppendText(msg+'\n')
        else:
            self.mainframe.log.AppendText(unicode(msg, 'utf-8', errors='ignore')+'\n')

    def email_settings(self, event):
        emailframe = gui.EmailSettings(self.mainframe, -1, '')
        emailframe.active.SetValue(self.dc.get('email_active', 'False')=='True')
        emailframe.smtp_server.SetValue(self.dc.get('email_smtp', 'smtp.example.com'))
        emailframe.from_address.SetValue(self.dc.get('email_from', 'from@example.com'))
        emailframe.to_address.SetValue(self.dc.get('email_to', 'to@example.com'))
        def cancel(evt):
            emailframe.Close()
        def save(evt):
            active = 'False'
            if emailframe.active.IsChecked():
                active = 'True'
            self.dc.set('email_active', active)
            self.dc.set('email_smtp', emailframe.smtp_server.GetValue())
            self.dc.set('email_from', emailframe.from_address.GetValue())
            self.dc.set('email_to', emailframe.to_address.GetValue())
            emailframe.Close()
        def closing(evt):
            emailframe.MakeModal(False)
            evt.Skip()
        emailframe.Bind(wx.EVT_BUTTON, cancel, emailframe.cancel)
        emailframe.Bind(wx.EVT_BUTTON, save, emailframe.save)
        emailframe.Bind(wx.EVT_CLOSE, closing, emailframe)
        emailframe.Show(True)
        emailframe.MakeModal(True)

    def choose_destination(self, event):
        dialog = wx.DirDialog(self.mainframe, 'Choose a directory', style=wx.DD_DEFAULT_STYLE|wx.DD_NEW_DIR_BUTTON)
        if dialog.ShowModal() == wx.ID_OK:
            if not os.path.exists(os.path.join(dialog.GetPath(), 'backups')):
                msg = 'You have selected a directory that has not previously been used for backups (it doesnt contain a "backups"-folder). Do you want me to create this for you?'
                dlg = wx.MessageDialog(self.mainframe, msg, 'Initialize new directory?', wx.YES_NO|wx.ICON_QUESTION)
                if dlg.ShowModal() != wx.ID_YES:
                    return self.choose_destination(event)
            self.dc.set('destination', dialog.GetPath())
        self.update_gui()
        event.Skip()

    def add_source(self, event):
        dialog = wx.DirDialog(self.mainframe, 'Choose a directory', style=wx.DD_DEFAULT_STYLE)
        if dialog.ShowModal() == wx.ID_OK:
            self.dc.add_source(dialog.GetPath())
            self.mainframe.sources_list.InsertStringItem(self.mainframe.sources_list.GetItemCount(), dialog.GetPath())
        event.Skip()

    def remove_source(self, event):
        item = -1
        delete_ids = []
        while True:
            item = self.mainframe.sources_list.GetNextItem(item, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED)
            if item == -1:
                break
            delete_ids.append(item)
        for item in delete_ids:
            listitem = self.mainframe.sources_list.GetItem(item)
            self.dc.remove_source(listitem.GetText())
            self.mainframe.sources_list.DeleteItem(item)
        event.Skip()

    def update_gui(self):
        self.mainframe.destination_label.SetValue(self.dc.get('destination', default='Unknown'))

        # only update sources if we havent done so before
        if self.mainframe.sources_list.GetItemCount() == 0:
            for item in self.dc.get_sources():
                self.mainframe.sources_list.InsertStringItem(self.mainframe.sources_list.GetItemCount(), item)

    statistics = 'nothing to see here'
    def generate_statistics(self, event):
        stats = ''
        dest = self.dc.get('destination', False)
        if dest and os.path.exists(dest):
            stats += 'Number of backups: %s.\n'%len(self.bc.get_current_backups())
            try:
                stats += 'Oldest backup: %s\n'%self.bc.get_oldest(self.bc.get_current_backups())
                stats += 'Newest backup: %s\n'%self.bc.get_newest(self.bc.get_current_backups())
            except:
                pass
            stats += 'Free space on destination device: %s\n'%(pretty_number(self.bc.get_free_space()))
            stats += '\n'
            stats += '\n'
            stats += 'Stored backups:\n'
            for backup in self.bc.get_current_backups():
                stats += backup+'\n'
        self.mainframe.statistics_text.SetLabel(stats)
        self.mainframe.Refresh()

    def run(self, event, send_email=True):
        message = []
        def message_appender(text):
            message.append(text.strip()+'\n')
        logger.observers.append(message_appender)
        try:
            self.bc.perform_backup()
        finally:
            logger.observers.remove(message_appender)
            
            active = self.dc.get('email_active', None) == 'True'
            server = self.dc.get('email_smtp', None)
            from_ = self.dc.get('email_from', None)
            to = self.dc.get('email_to', None)
            
            
            if send_email and active and server and from_ and to:
                logger.msg('Sending email notification:')
                mail.send_mail(server, from_, to, message[-1], ' '.join(message))
            else:
                logger.msg('Not sending email notification.')

    def mainloop(self):
        self.app.MainLoop()

class BackupController(object):
    def __init__(self, dc, gui=True):
        self.dc = dc
        self.gui = gui
        self.total_size = 0
        self.remaining = 0


    def get_destination(self):
        # make sure destination is ok:
        destination = self.dc.get('destination')
        absdest = os.path.normpath(os.path.abspath(destination))
        backupdir = os.path.join(absdest, 'backups')
        if not os.path.exists(backupdir):
            os.makedirs(backupdir)
        return backupdir

    def get_current_backups(self):
        destination = self.get_destination()
        return [os.path.normpath(os.path.join(destination, name)) for name in os.listdir(destination)]

    def get_oldest(self, directories):
        if len(directories) == 0:
            raise Exception('No directories found.')
        modified = [(path, os.stat(path).st_mtime) for path in directories]
        modified.sort(lambda a,b: cmp(a[1],b[1]))
        return modified[0][0]

    def get_newest(self, directories):
        if len(directories) == 0:
            raise Exception('No directories found.')
        modified = [(path, os.stat(path).st_mtime) for path in directories]
        modified.sort(lambda a,b: cmp(a[1],b[1]), reverse=True)
        return modified[0][0]

    def get_size(self, directory):
        total = os.path.getsize(directory)
        if not os.path.isdir(directory):
            return total
        for item in os.listdir(directory):
            path = os.path.join(directory, item)
            if os.path.isdir(path):
                total += self.get_size(path)
            else:
                total += os.path.getsize(path)
        return total

    def get_free_space(self, free_percentage=0.94):
        folder = self.get_destination()
        if platform.system() == 'Windows':
            free_bytes = ctypes.c_ulonglong(0)
            ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(folder), None, None, ctypes.pointer(free_bytes))
            return int(free_bytes.value * free_percentage)
        else:
            return int(os.statvfs(folder).f_bfree * free_percentage)

    def get_directory_for_date(self, date):
        return 'backup-%s-%s-%s'%(date.year,date.month,date.day)

    def get_todays_destination(self):
        destination = self.get_destination()
        today = datetime.date.today()
        return os.path.join(destination, self.get_directory_for_date(today))

    @catch_all_and_log(show_error=True)
    def perform_backup(self, send_mail=False):
        start = datetime.datetime.now()
        logger.msg('Starting backup run at %s'%start)
        logger.msg('-'*20)
        dest = self.get_todays_destination()
        logger.msg('Performing backup to',dest)
        self.total_size = self.remaining = source_size = sum([self.get_size(path) for path in self.get_sources()])
        if self.gui:
            self.dialog = wx.ProgressDialog('Performing backup', 'time remaining', self.remaining, style=wx.PD_ELAPSED_TIME|wx.PD_REMAINING_TIME|wx.PD_CAN_ABORT)
        try:
            while self.get_free_space() <= source_size:
                logger.msg('Need',pretty_number(source_size),'to start copying, but have',pretty_number(self.get_free_space()),'free.')
                old = self.get_oldest(self.get_current_backups())
                util.rmtree(old)
            logger.msg('Copying',pretty_number(source_size),'and have',pretty_number(self.get_free_space()),'of free space left.')
            logger.msg('')
            logger.msg('-'*20)
            for source in self.get_sources():
                self.deepcopy(source, dest)
        except Exception, e:
                logger.msg('-'*20)
                logger.msg('')
                end = datetime.datetime.now()
                logger.msg('Backup FAILED after %s.'%(end-start))
                raise
        finally:
            if self.gui: 
                self.dialog.Update(self.total_size, 'Done')
                self.dialog.Destroy()
        logger.msg('-'*20)
        logger.msg('')
        end = datetime.datetime.now()
        logger.msg('Backup completed SUCCESSFULLY in %s.'%(end-start))

    def deepcopy(self, src, dest):
        def copier(dest, dirname, filenames):
            safe_dirname = dirname.replace(':', '_', 1)
            # TODO: find out why the next line isnt working
            #normdest = os.path.normpath(os.path.join(dest, safe_dirname))
            normdest = os.path.normpath(os.path.sep.join([dest, safe_dirname]))
            if not os.path.exists(normdest):
                os.makedirs(normdest)
                util.copystat(dirname, normdest)
            logger.msg(dirname+' --> '+normdest)
            for filename in filenames:
                src = os.path.join(dirname, filename)
                logger.msg(src+' --> '+os.path.join(normdest,filename))
                if self.gui:
                    cont = self.dialog.Update(min(self.total_size-self.remaining,self.total_size-1), src)
                    if not cont:
                        raise Exception('Copy aborted by user.')
                if os.path.isdir(src):
                    pass # this will be handled in a later os.path.walk call to this copier
                else:
                    util.copy2(src, os.path.join(normdest,filename))
                    self.remaining -= self.get_size(src)
        os.path.walk(os.path.abspath(src), copier, dest)

    def get_sources(self):
        return [os.path.normpath(os.path.abspath(source)) for source in self.dc.get_sources()]

class Logger(object):
    # TODO: Handle different character encodings...
    def __init__(self):
        self.observers = []
    def as_unicode(self, data):
        try:
            return unicode(data)
        except:
            if isinstance(data, unicode):
                return data
            return unicode(data, errors='ignore')
    def msg(self, *msg):
        str_msg = [self.as_unicode(x) for x in msg]
        msg = ' '.join(str_msg)
        for observer in self.observers:
            observer(msg.encode('utf-8', 'ignore'))
    def err(self, *msg):
        str_msg = [str(x) for x in msg]
        msg = 'ERR: '+' '.join(str_msg)
        # mangle into utf-8 encoded str:
        msg = unicode(msg, errors='ignore').encode('utf-8')
        for observer in self.observers:
            observer(msg)

logger = Logger()

if __name__ == '__main__':
    c = Controller()
    c.mainloop()
